﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using MugenMvvmToolkit.Attributes;
using MugenMvvmToolkit.Interfaces;
using MugenMvvmToolkit.Models;
using MugenMvvmToolkit.Utils;

namespace MugenMvvmToolkit.Infrastructure
{
    /// <summary>
    ///     Represents the <see cref="IViewMappingProvider" /> which use attributes to get mappings.
    /// </summary>
    public class AttributeViewPageMappingProvider : IViewPageMappingProvider
    {
        #region Fields

        private readonly Dictionary<Type, Dictionary<string, IViewPageMappingItem>> _viewModelToMapping;
        private readonly Dictionary<Type, IViewPageMappingItem> _viewTypeToMapping;

        #endregion

        #region Constructors

        /// <summary>
        ///     Initializes a new instance of the <see cref="AttributeViewPageMappingProvider" /> class.
        /// </summary>
        public AttributeViewPageMappingProvider([NotNull]IEnumerable<Assembly> assemblies)
        {
            Should.NotBeNull(assemblies, "assemblies");
            _viewTypeToMapping = new Dictionary<Type, IViewPageMappingItem>();
            _viewModelToMapping = new Dictionary<Type, Dictionary<string, IViewPageMappingItem>>();
            InitializePageMappings(assemblies);
        }

        #endregion

        #region Methods

        private void InitializePageMappings(IEnumerable<Assembly> assemblies)
        {
            foreach (Assembly assembly in assemblies)
            {
                try
                {
                    foreach (Type type in assembly.GetTypes())
                    {
#if !NETFX_CORE
                        if (type.IsAbstract || type.IsInterface) continue;
                        PageViewModelAttribute[] viewModelAttributes = type
                            .GetCustomAttributes(typeof(PageViewModelAttribute), true)
                            .OfType<PageViewModelAttribute>()
                            .ToArray();
#else
                        var typeInfo = type.GetTypeInfo();
                        if (typeInfo.IsAbstract || typeInfo.IsInterface) continue;
                        var viewModelAttributes = typeInfo
                                                    .GetCustomAttributes(typeof(PageViewModelAttribute), true)
                                                    .OfType<PageViewModelAttribute>()
                                                    .ToArray();
#endif

                        if (viewModelAttributes.Length == 0) continue;
                        Should.BeView(type, "type");
                        foreach (PageViewModelAttribute attribute in viewModelAttributes)
                        {
                            AddMapping(new ViewPageMappingItem(GetUri(type, attribute.ViewModelType, attribute.Url,
                                attribute.UriKind), attribute.ViewModelType, type, attribute.Name));
                        }
                    }
                }
                catch (ReflectionTypeLoadException)
                {
                    if (!GlobalSettings.IsDesignMode)
                        throw;
                }
                catch (TypeLoadException)
                {
                    if (!GlobalSettings.IsDesignMode)
                        throw;
                }
                catch (BadImageFormatException)
                {
                    if (!GlobalSettings.IsDesignMode)
                        throw;
                }
            }
        }

        /// <summary>
        ///     Creates an <see cref="Uri" /> for the specified type of view.
        /// </summary>
        /// <param name="viewType">The specified type of view.</param>
        /// <param name="viewModelType">The specified type of view model.</param>
        /// <param name="url">The specified url value.</param>
        /// <param name="uriKind">
        ///     The specified <see cref="UriKind" />.
        /// </param>
        /// <returns>
        ///     An instance of <see cref="Uri" />.
        /// </returns>
        protected virtual Uri GetUri(Type viewType, Type viewModelType, string url, UriKind uriKind)
        {
            if (!string.IsNullOrEmpty(url))
                return new Uri(url, uriKind);
#if !NETFX_CORE
            var assembly = viewType.Assembly;
#else
            var assembly = viewType.GetTypeInfo().Assembly;
#endif
            string name = assembly.GetAssemblyName();
            string uri = viewType.FullName.Replace(name, string.Empty).Replace(".", "/");
            return new Uri(string.Format("/{0};component{1}.xaml", name, uri), uriKind);
        }

        private void AddMapping(IViewPageMappingItem mappingItem)
        {
            if (_viewTypeToMapping.ContainsKey(mappingItem.ViewType))
                Thrower.DuplicateViewModelMapping(mappingItem.ViewType);

            _viewTypeToMapping.Add(mappingItem.ViewType, mappingItem);
            Dictionary<string, IViewPageMappingItem> value;
            if (!_viewModelToMapping.TryGetValue(mappingItem.ViewModelType, out value))
            {
                value = new Dictionary<string, IViewPageMappingItem>();
                _viewModelToMapping[mappingItem.ViewModelType] = value;
            }
            IViewPageMappingItem item;
            string name = mappingItem.Name ?? string.Empty;
            if (value.TryGetValue(name, out item))
                Thrower.DuplicateViewMapping(item.ViewType, item.ViewModelType, item.Name);
            value[name] = mappingItem;
        }

        #endregion

        #region Implementation of IViewPageMappingProvider

        /// <summary>
        ///     Gets a series instances of <see cref="IViewPageMappingItem" />.
        /// </summary>
        public IEnumerable<IViewPageMappingItem> ViewPageMappings
        {
            get { return _viewTypeToMapping.Values; }
        }

        /// <summary>
        ///     Finds the <see cref="IViewPageMappingItem" /> for the specified type of view.
        /// </summary>
        /// <param name="viewType">The specified type of view.</param>
        /// <param name="throwOnError">true to throw an exception if the type cannot be found; false to return null. Specifying false also suppresses some other exception conditions, but not all of them.</param>
        /// <returns>
        ///     An instance of <see cref="IViewPageMappingItem" />.
        /// </returns>
        public IViewPageMappingItem FindMappingForView(Type viewType, bool throwOnError)
        {
            Should.NotBeNull(viewType, "viewType");
            IViewPageMappingItem item;
            if (!_viewTypeToMapping.TryGetValue(viewType, out item) && throwOnError)
                Thrower.ViewModelNotFound(viewType.FullName);
            return item;
        }

        /// <summary>
        ///     Finds the <see cref="IViewPageMappingItem" /> for the specified type of view model.
        /// </summary>
        /// <param name="viewModelType">The specified type of view model.</param>
        /// <param name="viewName">The specified name of view, if any.</param>
        /// <param name="throwOnError">true to throw an exception if the type cannot be found; false to return null. Specifying false also suppresses some other exception conditions, but not all of them.</param>
        /// <returns>
        ///     An instance of <see cref="IViewPageMappingItem" />.
        /// </returns>
        public IViewPageMappingItem FindMappingForViewModel(Type viewModelType, string viewName, bool throwOnError)
        {
            Should.NotBeNull(viewModelType, "viewModelType");
            Should.BeViewModel(viewModelType, "viewModelType");
            viewName = viewName ?? string.Empty;
            Dictionary<string, IViewPageMappingItem> value;
            if (!_viewModelToMapping.TryGetValue(viewModelType, out value))
            {
#if !NETFX_CORE
                if (viewModelType.IsGenericType)
#else
                if (viewModelType.GetTypeInfo().IsGenericType)
#endif
                {
                    viewModelType = viewModelType.GetGenericTypeDefinition();
                    if (viewModelType != null)
                        _viewModelToMapping.TryGetValue(viewModelType, out value);
                }
            }
            if (value == null)
            {
                if (throwOnError)
                    Thrower.ViewNotFound(viewModelType);
                return null;
            }
            IViewPageMappingItem item;
            if (!value.TryGetValue(viewName, out item))
            {
                if (viewName != string.Empty)
                {
                    value.TryGetValue(string.Empty, out item);
                }
            }
            if (item == null && throwOnError)
                Thrower.ViewNotFound(viewModelType);
            return item;
        }

        #endregion
    }
}